package cn.snmp.mutual.org.opennms.protocols.snmp.asn1;

import java.math.*;

public class BerEncoder implements AsnEncoder {
    private static final byte LONG_LENGTH = Byte.MIN_VALUE;
    private static final byte HIGH_BIT = Byte.MIN_VALUE;
    private static final byte EXTENSION_ID = 31;
    private static final byte CONSTRUCTOR = 32;

    protected static int byteToInt(final byte b) {
        return (b < 0) ? (256 + b) : b;
    }

    protected static long byteToLong(final byte b) {
        return (b < 0) ? (256L + b) : b;
    }

    protected static boolean isConstructor(final byte b) {
        return (b & 0x20) == 0x20;
    }

    protected static boolean isExtensionId(final byte b) {
        return (b & 0x1F) == 0x1F;
    }

    protected static void copy(final byte[] src, final int srcOff, final byte[] dest, final int destOff, final int bytesToCopy) throws ArrayIndexOutOfBoundsException {
        if (dest.length - destOff < bytesToCopy || src.length - srcOff < bytesToCopy) {
            throw new ArrayIndexOutOfBoundsException("Destination or source buffer is insufficent");
        }
        for (int x = bytesToCopy - 1; x >= 0; --x) {
            dest[destOff + x] = src[srcOff + x];
        }
    }

    protected static void copy(final int[] src, final int srcOff, final int[] dest, final int destOff, final int intsToCopy) throws ArrayIndexOutOfBoundsException {
        if (dest.length - destOff < intsToCopy || src.length - srcOff < intsToCopy) {
            throw new ArrayIndexOutOfBoundsException("Destination or source buffer is insufficent");
        }
        for (int x = intsToCopy - 1; x >= 0; --x) {
            dest[destOff + x] = src[srcOff + x];
        }
    }

    protected static void rotate(final byte[] buf, final int begin, final int pivot, final int end) throws ArrayIndexOutOfBoundsException {
        final int dist = end - pivot;
        final byte[] hold = new byte[dist];
        copy(buf, pivot, hold, 0, dist);
        for (int x = pivot - begin - 1; x >= 0; --x) {
            buf[begin + dist + x] = buf[begin + x];
        }
        copy(hold, 0, buf, begin, dist);
    }

    public int buildLength(final byte[] buf, int startOffset, final int asnLength) throws AsnEncodingException {
        if (asnLength <= 127) {
            if (buf.length - startOffset < 1) {
                throw new AsnEncodingException("Buffer overflow error");
            }
            buf[startOffset++] = (byte) (asnLength & 0x7F);
        } else if (asnLength <= 255) {
            if (buf.length - startOffset < 2) {
                throw new AsnEncodingException("Buffer overflow error");
            }
            buf[startOffset++] = -127;
            buf[startOffset++] = (byte) (asnLength & 0xFF);
        } else {
            if (buf.length - startOffset < 3) {
                throw new AsnEncodingException("Buffer overflow error");
            }
            buf[startOffset++] = -126;
            buf[startOffset++] = (byte) (asnLength >>> 8 & 0xFF);
            buf[startOffset++] = (byte) (asnLength & 0xFF);
        }
        return startOffset;
    }

    public Object[] parseLength(final byte[] buf, int startOffset) throws AsnDecodingException {
        if (buf.length - startOffset < 1) {
            throw new AsnDecodingException("Buffer underflow error");
        }
        final Object[] retVals = new Object[2];
        byte numBytes = buf[startOffset++];
        if ((numBytes & 0xFFFFFF80) == 0x0) {
            numBytes &= 0x7F;
            retVals[1] = new Integer(byteToInt(numBytes));
        } else {
            numBytes &= 0x7F;
            if (numBytes == 1) {
                if (buf.length - startOffset < 1) {
                    throw new AsnDecodingException("Buffer underflow error");
                }
                retVals[1] = new Integer(byteToInt(buf[startOffset++]));
            } else {
                if (numBytes != 2) {
                    throw new AsnDecodingException("Invalid ASN.1 length");
                }
                if (buf.length - startOffset < 2) {
                    throw new AsnDecodingException("Buffer underflow error");
                }
                final int val = byteToInt(buf[startOffset++]) << 8 | byteToInt(buf[startOffset++]);
                retVals[1] = new Integer(val);
            }
        }
        retVals[0] = new Integer(startOffset);
        return retVals;
    }

    public int buildHeader(final byte[] buf, int startOffset, final byte asnType, final int asnLength) throws AsnEncodingException {
        if (buf.length - startOffset < 1) {
            throw new AsnEncodingException("Buffer overflow error");
        }
        buf[startOffset++] = asnType;
        return this.buildLength(buf, startOffset, asnLength);
    }

    public Object[] parseHeader(final byte[] buf, int startOffset) throws AsnDecodingException {
        if (buf.length - startOffset < 1) {
            throw new AsnDecodingException("Insufficent buffer length");
        }
        final byte asnType = buf[startOffset++];
        if (isExtensionId(asnType)) {
            throw new AsnDecodingException("Buffer underflow error");
        }
        final Object[] lenVals = this.parseLength(buf, startOffset);
        final Object[] rVals = {lenVals[0], new Byte(asnType), lenVals[1]};
        return rVals;
    }

    public int buildInteger32(final byte[] buf, int startOffset, final byte asnType, int asnInt32) throws AsnEncodingException {
        int mask;
        int intSz;
        for (mask = -8388608, intSz = 4; ((asnInt32 & mask) == 0x0 || (asnInt32 & mask) == mask) && intSz > 1; --intSz, asnInt32 <<= 8) {
        }
        startOffset = this.buildHeader(buf, startOffset, asnType, intSz);
        if (buf.length - startOffset < intSz) {
            throw new AsnEncodingException("Insufficent buffer size");
        }
        mask = -16777216;
        while (intSz-- > 0) {
            final byte b = (byte) ((asnInt32 & mask) >>> 24);
            buf[startOffset++] = b;
            asnInt32 <<= 8;
        }
        return startOffset;
    }

    public Object[] parseInteger32(final byte[] buf, int startOffset) throws AsnDecodingException {
        final Object[] hdrVals = this.parseHeader(buf, startOffset);
        startOffset = (int) hdrVals[0];
        final Byte asnType = (Byte) hdrVals[1];
        int asnLength = (int) hdrVals[2];
        if (buf.length - startOffset < asnLength) {
            throw new AsnDecodingException("Buffer underflow error");
        }
        if (asnLength > 4) {
            throw new AsnDecodingException("Integer too large: cannot decode");
        }
        int asnValue = 0;
        if ((buf[startOffset] & 0xFFFFFF80) == 0xFFFFFF80) {
            asnValue = -1;
        }
        while (asnLength-- > 0) {
            asnValue = (asnValue << 8 | byteToInt(buf[startOffset++]));
        }
        final Object[] rVals = {new Integer(startOffset), asnType, new Integer(asnValue)};
        return rVals;
    }

    public int buildUInteger32(final byte[] buf, int startOffset, final byte asnType, long asnUInt32) throws AsnEncodingException {
        long mask = 4286578688L;
        int intSz = 4;
        boolean bAddNullByte = false;
        if (asnUInt32 > 2147483647L) {
            bAddNullByte = true;
            ++intSz;
        }
        while ((asnUInt32 & mask) == 0x0L && intSz > 1) {
            --intSz;
            asnUInt32 <<= 8;
        }
        startOffset = this.buildHeader(buf, startOffset, asnType, intSz);
        if (buf.length - startOffset < intSz) {
            throw new AsnEncodingException("Buffer overflow error");
        }
        if (bAddNullByte) {
            buf[startOffset++] = 0;
            --intSz;
        }
        mask = 4278190080L;
        while (intSz-- > 0) {
            final byte b = (byte) ((asnUInt32 & mask) >>> 24);
            buf[startOffset++] = b;
            asnUInt32 <<= 8;
        }
        return startOffset;
    }

    public Object[] parseUInteger32(final byte[] buf, int startOffset) throws AsnDecodingException {
        final Object[] hdrVals = this.parseHeader(buf, startOffset);
        startOffset = (int) hdrVals[0];
        final Byte asnType = (Byte) hdrVals[1];
        int asnLength = (int) hdrVals[2];
        if (buf.length - startOffset < asnLength) {
            throw new AsnDecodingException("Buffer underflow error");
        }
        if (asnLength > 5) {
            throw new AsnDecodingException("Integer too large: cannot decode");
        }
        long asnValue = 0L;
        if ((buf[startOffset] & 0xFFFFFF80) == 0xFFFFFF80) {
            asnValue = -1L;
        }
        while (asnLength-- > 0) {
            asnValue = (asnValue << 8 | byteToLong(buf[startOffset++]));
        }
        asnValue &= 0xFFFFFFFFL;
        final Object[] rVals = {new Integer(startOffset), asnType, new Long(asnValue)};
        return rVals;
    }

    public int buildUInteger64(final byte[] buf, int startOffset, final byte asnType, final BigInteger asnUInt64) throws AsnEncodingException {
        final byte[] bytes = asnUInt64.toByteArray();
        startOffset = this.buildHeader(buf, startOffset, asnType, bytes.length);
        if (buf.length - startOffset < bytes.length) {
            throw new AsnEncodingException("Buffer overflow error");
        }
        for (int i = 0; i < bytes.length; ++i) {
            buf[startOffset++] = bytes[i];
        }
        return startOffset;
    }

    public Object[] parseUInteger64(final byte[] buf, int startOffset) throws AsnDecodingException {
        final Object[] hdrVals = this.parseHeader(buf, startOffset);
        startOffset = (int) hdrVals[0];
        final Byte asnType = (Byte) hdrVals[1];
        final int asnLength = (int) hdrVals[2];
        if (buf.length - startOffset < asnLength) {
            throw new AsnDecodingException("Buffer underflow error");
        }
        if (asnLength > 9) {
            throw new AsnDecodingException("Integer too large: cannot decode");
        }
        final byte[] asnBuf = new byte[asnLength];
        for (int i = 0; i < asnLength; ++i) {
            asnBuf[i] = buf[startOffset++];
        }
        final BigInteger asnValue = new BigInteger(asnBuf);
        final Object[] rVals = {new Integer(startOffset), asnType, asnValue};
        return rVals;
    }

    public int buildNull(final byte[] buf, final int startOffset, final byte asnType) throws AsnEncodingException {
        return this.buildHeader(buf, startOffset, asnType, 0);
    }

    public Object[] parseNull(final byte[] buf, final int startOffset) throws AsnDecodingException {
        final Object[] hdrVals = this.parseHeader(buf, startOffset);
        if ((int) hdrVals[2] != 0) {
            throw new AsnDecodingException("Malformed ASN.1 Type");
        }
        final Object[] rVals = {hdrVals[0], hdrVals[1]};
        return rVals;
    }

    public int buildString(final byte[] buf, int startOffset, final byte asnType, final byte[] opaque) throws AsnEncodingException {
        final int asnLength = opaque.length;
        startOffset = this.buildHeader(buf, startOffset, asnType, asnLength);
        if (buf.length - startOffset < opaque.length) {
            throw new AsnEncodingException("Insufficent buffer length");
        }
        try {
            copy(opaque, 0, buf, startOffset, opaque.length);
        } catch (ArrayIndexOutOfBoundsException err) {
            throw new AsnEncodingException("Buffer overflow error");
        }
        return startOffset + opaque.length;
    }

    public Object[] parseString(final byte[] buf, int startOffset) throws AsnDecodingException {
        final Object[] hdrVals = this.parseHeader(buf, startOffset);
        startOffset = (int) hdrVals[0];
        final Byte asnType = (Byte) hdrVals[1];
        final int asnLength = (int) hdrVals[2];
        if (buf.length - startOffset < asnLength) {
            throw new AsnDecodingException("Insufficent buffer length");
        }
        final byte[] opaque = new byte[asnLength];
        try {
            copy(buf, startOffset, opaque, 0, asnLength);
        } catch (ArrayIndexOutOfBoundsException err) {
            throw new AsnDecodingException("Buffer underflow exception");
        }
        final Object[] rVals = {new Integer(startOffset + asnLength), asnType, opaque};
        return rVals;
    }

    public int buildObjectId(final byte[] buf, int startOffset, final byte asnType, final int[] oids) throws AsnEncodingException {
        if (buf.length - startOffset < 1) {
            throw new AsnEncodingException("Buffer overflow error");
        }
        int[] toEncode = oids;
        final int begin = startOffset;
        if (oids.length < 2) {
            toEncode = new int[]{0, 0};
        }
        if (toEncode[0] < 0 || toEncode[0] > 2) {
            throw new AsnEncodingException("Invalid Object Identifier");
        }
        if (toEncode[1] < 0 || toEncode[1] > 40) {
            throw new AsnEncodingException("Invalid Object Identifier");
        }
        buf[startOffset++] = (byte) (toEncode[0] * 40 + toEncode[1]);
        int oidNdx = 2;
        while (oidNdx < toEncode.length) {
            final int oid = toEncode[oidNdx++];
            if (oid < 127) {
                if (buf.length - startOffset < 1) {
                    throw new AsnEncodingException("Buffer overflow error");
                }
                buf[startOffset++] = (byte) oid;
            } else {
                int mask = 0;
                int bits = 0;
                int tmask;
                int tbits;
                for (tmask = 0, tbits = 0, tmask = 127, tbits = 0; tmask != 0; tmask <<= 7, tbits += 7) {
                    if ((oid & tmask) != 0x0) {
                        mask = tmask;
                        bits = tbits;
                    }
                }
                while (mask != 127) {
                    if (buf.length - startOffset < 1) {
                        throw new AsnEncodingException("Buffer overflow error");
                    }
                    buf[startOffset++] = (byte) ((oid & mask) >>> bits | 0xFFFFFF80);
                    mask >>>= 7;
                    bits -= 7;
                    if (mask != 31457280) {
                        continue;
                    }
                    mask = 266338304;
                }
                if (buf.length - startOffset < 1) {
                    throw new AsnEncodingException("Insufficent buffer space");
                }
                buf[startOffset++] = (byte) (oid & mask);
            }
        }
        final int pivot = startOffset;
        final int asnLength = pivot - begin;
        final int end = this.buildHeader(buf, pivot, asnType, asnLength);
        try {
            rotate(buf, begin, pivot, end);
        } catch (ArrayIndexOutOfBoundsException err) {
            throw new AsnEncodingException("Insufficent buffer space");
        }
        return end;
    }

    public Object[] parseObjectId(final byte[] buf, int startOffset) throws AsnDecodingException {
        final Object[] hdrVals = this.parseHeader(buf, startOffset);
        startOffset = (int) hdrVals[0];
        final Byte asnType = (Byte) hdrVals[1];
        int asnLength = (int) hdrVals[2];
        if (buf.length - startOffset < asnLength) {
            throw new AsnDecodingException("Buffer underflow error");
        }
        if (asnLength == 0) {
            final int[] ids = new int[2];
            ids[0] = (ids[1] = 0);
            final Object[] rVals = {new Integer(startOffset), asnType, ids};
            return rVals;
        }
        int idsOff = 0;
        final int[] ids2 = new int[asnLength + 1];
        --asnLength;
        int oid = byteToInt(buf[startOffset++]);
        ids2[idsOff++] = oid / 40;
        ids2[idsOff++] = oid % 40;
        while (asnLength > 0) {
            oid = 0;
            boolean done = false;
            do {
                --asnLength;
                final byte b = buf[startOffset++];
                oid = (oid << 7 | (b & 0x7F));
                if ((b & 0xFFFFFF80) == 0x0) {
                    done = true;
                }
            } while (!done);
            ids2[idsOff++] = oid;
        }
        int[] retOids;
        if (idsOff == ids2.length) {
            retOids = ids2;
        } else {
            retOids = new int[idsOff];
            copy(ids2, 0, retOids, 0, idsOff);
        }
        final Object[] rVals2 = {new Integer(startOffset), asnType, retOids};
        return rVals2;
    }
}
